/*
 *   Copyright 2008 Francois Fernandes
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

package com.googlecode.opendoclibrary.server.internal.core;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

import com.googlecode.opendoclibrary.library.spi.IDocumentId;

/**
 * @author fernanfs
 * 
 */
public class DocumentIdFacotry implements IDocumentIdFactory
{
    private static class DocumentId implements IDocumentId
    {
        private final byte[] id;

        public DocumentId( byte[] id )
        {
            super();
            if ( id.length != 16 )
            {
                throw new IllegalArgumentException( "document id has to consist of 16 bytes" );
            }
            this.id = id;
        }

        /*
         * (non-Javadoc)
         * 
         * @see com.googlecode.opendoclibrary.library.spi.IDocumentId#asBytes()
         */
        @Override
        public byte[] asBytes()
        {
            return id.clone();
        }

        private String idString;

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString()
        {
            return asString();
        }

        /*
         * (non-Javadoc)
         * 
         * @see com.googlecode.opendoclibrary.library.spi.IDocumentId#asString()
         */
        @Override
        public String asString()
        {
            if ( idString == null )
            {
                StringBuilder sb = new StringBuilder();
                hex( sb, id[0] );
                hex( sb, id[1] );
                hex( sb, id[2] );
                hex( sb, id[3] );
                sb.append( '-' );
                hex( sb, id[4] );
                hex( sb, id[5] );
                hex( sb, id[6] );
                hex( sb, id[7] );
                sb.append( '-' );
                hex( sb, id[8] );
                hex( sb, id[9] );
                hex( sb, id[10] );
                hex( sb, id[11] );
                sb.append( '-' );
                hex( sb, id[12] );
                hex( sb, id[13] );
                hex( sb, id[14] );
                hex( sb, id[15] );
                idString = sb.toString();
            }
            return idString;
        }

        protected void hex( StringBuilder sb, byte val )
        {
            sb.append( HEX_CHARS[( val >> 4 ) & 0x0f] );
            sb.append( HEX_CHARS[val & 0x0f] );
        }

        private static final char[] HEX_CHARS =
            { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
    }

    private AtomicLong previousTime;

    private SecureRandom rand;

    /**
     * 
     */
    public DocumentIdFacotry()
    {
        previousTime = new AtomicLong();
        rand = new SecureRandom();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.googlecode.opendoclibrary.server.internal.core.IDocumentIDFactory#generate()
     */
    @Override
    public IDocumentId generate()
    {

        long timePart;
        while ( ( timePart = createTime() ) <= 0 )
            ;

        long randomPart = rand.nextLong();

        timePart = Long.reverse( timePart );

        byte[] data = new byte[16];
        fill( data, 0, timePart );
        fill( data, 8, randomPart );
        return new DocumentId( data );
    }

    private void fill( byte[] data, int off, long value )
    {
        data[off + 0] = (byte) ( value >> ( 7 * 8 ) );
        data[off + 1] = (byte) ( value >> ( 6 * 8 ) );
        data[off + 2] = (byte) ( value >> ( 5 * 8 ) );
        data[off + 3] = (byte) ( value >> ( 4 * 8 ) );
        data[off + 4] = (byte) ( value >> ( 3 * 8 ) );
        data[off + 5] = (byte) ( value >> ( 2 * 8 ) );
        data[off + 6] = (byte) ( value >> 8 );
        data[off + 7] = (byte) value;
    }

    protected long createTime()
    {
        long cur = System.currentTimeMillis();
        long prev = previousTime.get();
        if ( prev < cur )
            if ( previousTime.compareAndSet( prev, cur ) )
                return cur;
        return -1;
    }

    public static void main( String[] args )
    {
        DocumentIdFacotry f = new DocumentIdFacotry();

        ThreadGroup tg = new ThreadGroup( "generator threads" );
        List<IDocumentId> ids = new ArrayList<IDocumentId>( 10 * 1000 );
        List<Long> durations = new ArrayList<Long>( 10 * 1000 );
        GeneratorThread[] threads = new GeneratorThread[10];
        for ( int i = 0; i < threads.length; i++ )
        {
            threads[i] = new GeneratorThread( tg, f, ids, durations );
        }

        for ( GeneratorThread gt : threads )
        {
            gt.start();
        }

        for ( GeneratorThread gt : threads )
        {
            try
            {
                gt.join();
            }
            catch ( InterruptedException e )
            {
            }
        }

        HashSet<String> duplicationCheck = new HashSet<String>();

        for ( IDocumentId id : ids )
        {
            String s = id.asString();
            System.err.println( s );
            if ( duplicationCheck.contains( s ) )
                System.err.println( "DUPLICATION: " + s );
            else
                duplicationCheck.add( s );
        }
        
        long sum = 0;
        int count = 0;
        for ( Long dur : durations )
        {
            count++;
            sum += dur;
        }
        System.err.println( sum / count );
    }

    private static class GeneratorThread extends Thread
    {
        private DocumentIdFacotry factory;

        private List<IDocumentId> ids;

        private final List<Long> durations;

        public GeneratorThread( ThreadGroup tg, DocumentIdFacotry factory, List<IDocumentId> ids, List<Long> durations )
        {
            super( tg, "generator" );
            this.factory = factory;
            this.ids = ids;
            this.durations = durations;
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.lang.Thread#run()
         */
        @Override
        public void run()
        {
            IDocumentId[] generated = new IDocumentId[1000];
            long[] generationDuration = new long[generated.length];
            for ( int i = 0; i < generated.length; i++ )
            {
                long start = System.currentTimeMillis();
                generated[i] = factory.generate();
                long dur = System.currentTimeMillis() - start;
                generationDuration[i] = dur;
            }

            synchronized ( ids )
            {
                for ( IDocumentId docId : generated )
                {
                    ids.add( docId );
                }
            }
            synchronized ( durations )
            {
                for ( Long genDur : generationDuration )
                {
                    durations.add( genDur );
                }
            }
        }
    }
}
